背景差分でユーザを検出する
はじめに
現在、カフェのシステムでは、機械学習を用いて、カメラを用いて動画を撮影し、商品の前にいる人物の骨格や手を検出することで、どのユーザがどの商品を取り出したかを判定しています。
今までは、骨格検出モデルを用いてエッジデバイスで動画を推論処理(撮影した画像から映っている人物の骨格の座標を検出する処理)を実行する、という構成で処理をしていました。今後、エッジ側のデバイスの費用を下げたり、骨格検出以外の処理を増やすことを考えているため、エッジデバイスからクラウドに動画を送信し、クラウド側で様々な処理を実行する、という構成を検討しています。
前回までの記事で、エッジデバイスで動画をエンコードする方法、動画ファイルをクラウド(AWS Kinesis Video Streams)に送信する方法について記載しました。
課題
上記の記事では、動画を常時送信し続けることを考えていました。これは、ユーザが目の前にいないときでも送信し続けるため、判定に利用することがない無駄なデータを送ることになり、無駄な通信帯域・AWS利用料がかかる、という問題がありました。
目的
そこで今回は、ユーザいるときのみデータを送信することを目的に、ユーザがいるかどうかを判定する方法を考えます。とりあえず、今回は簡単に実装するために、OpenCVに実装されている背景差分による検出方法を利用してみました。
解決方法
背景抽出クラス:BackgroundSubtractor(OpenCV)
今回は、OpenCVのBackgroundSubtractorを利用しました。リンクにかかれている通り、BackgroundSubtractorはいくつかのサブクラスで継承されており、それぞれの背景差分のアルゴリズムが利用されています。本記事では、とりあえず試してみるために、こちらのページの一番上に記載されているBackgroundSubtractorMOGを使用しました。
BackgroundSubtractorMOGをPythonでインポートするには、通常のOpenCV(opencv-python)だけではなく、opencv_contrib(opencv-contrib-python)もインストールする必要がありました。例えば、Ubuntuであれば、以下のようにしてインストールできると思います。(自分がインストールするとき、いくつかエラーがでましたが、opencv-contrib-pythonのページの「Frequently Asked Questions」を見て解決できました。)
pip3 install opencv-contrib-python
実装コード
上記のBackgroundSubtractorを利用して、以下のように実装しました。(以下のコードは、同じカフェチームのSINさんが実装したものを改良したものです。)
from typing import Tuple, Optional import os import cv2 # 背景差分の面積のしきい値 BOX_VALID_PX_RATIO = 1 / 100 # 使用する背景差分のクラス BG_SUBTRACTOR_CLASS = cv2.bgsegm.createBackgroundSubtractorMOG # 検出できなかった場合の学習率 LEARNING_RATE_NOT_DETECTED = 0.0003 #(少し試行した結果、変更可) def detect_person_in_frame(subtractor, frame): # 差分マスクを取得 detect_mask = subtractor.apply(frame, learningRate=0) # 輪郭を検出 detect_contours = cv2.findContours( detect_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] # 一定の大きさ以下の輪郭を削除 detect_contours = list( filter(lambda x: cv2.contourArea(x) > frame.shape[0] * frame.shape[1] * BOX_VALID_PX_RATIO, detect_contours)) # 輪郭を囲む外接矩形を取得する detect_bboxes = list( map(lambda x: cv2.boundingRect(x), detect_contours)) # 一定の大きさ以下の外接矩形を無効化する detected = len(detect_bboxes) > 0 # 検出できた場合とできなかった場合とで、学習率を変える if not detected: subtractor.apply(frame) else: subtractor.apply(frame, learningRate=LEARNING_RATE_NOT_DETECTED) return detected, detect_mask, detect_bboxes def detect_person_in_frames(subtractor, frames): detected_list = [] for frame in frames: detected, detect_mask, detect_bboxes = detect_person_in_frame( subtractor, frame) detected_list.append(detected) return any(detected_list) if __name__ == "__main__": subtractor = cv2.bgsegm.createBackgroundSubtractorMOG() cap = cv2.VideoCapture(0) while True: # 画像を取得 ret, img = cap.read() # 人物を検出する detected, detect_mask, detect_bboxes = detect_person_in_frame( subtractor, img) print(detect_bboxes) # 結果を表示する color = (0, 0, 255) if detected else (255, 0, 0) cv2.rectangle(img, (0, 0), (img.shape[1], img.shape[0]), color, 8) for x, y, w, h in detect_bboxes: cv2.rectangle(img, (x, y), (x+w, y+h), (0, 0, 255), 2) cv2.imshow("img", img) cv2.imshow("detect_mask", detect_mask) key = cv2.waitKey(1) if key == 27: break
処理の流れとしては、以下のとおりです
- カメラから画像を取得する
- 人物を検出する
- 画像の差分を抽出する
- 差分の輪郭を検出し、面積が一定以下のものを削除する
- 残った輪郭を外接する矩形を取得する
- 検出できた場合とできなかった場合とで、学習率を変えて学習する
- 結果をウィンドウで表示する
結果
カフェで撮影した動画に対して、適用した結果が以下の動画のようです。左が入力動画と検出結果で、右が背景差分の様子です。ユーザがいるエリアを検出できていることがわかります。
また、検出結果によって学習率を変えないと、ユーザが同じ位置に留まってた際に、段々と背景として学習されしまい、背景差分として抽出されなくなりました。これに対し、検出できたときの学習率を小さくすることで、ユーザが同じ位置に留まっていても検出し続けられるようにできました。
また別のカメラでも、同様に背景差分で人物を検出し、検出できたときのみ動画をKinesis Video Streamsに送信してみました。結果、マネジメントコンソールのKinesis Video Streamsの画面から、人物がいるときのみ動画が送信されている様子を確認できました。下の流れとしては次の通りです。
- 最初はユーザがいないため、動画が送信されてこない(黒い画面)
- 途中からユーザが画面内に来たため、動画が送信されてくる
- 最後の方はユーザがいないため、動画が送信されてこない(画面が止まっている)
まとめ
OpenCVのBackgroundSubtractor(背景差分)を利用して、ユーザが画面内にいるどうかを簡易に判定しました。これによって、エッジからクラウドへの無駄の動画の送信を省き、通信量やクラウド利用料を削減できました。